/*
 * 
 * Features: process action (abstract, need to be implemented in extended class)
 *           next piece queue (implemented)
 */
package sirtet;

import sirtet.event.*;
import java.awt.Point;
import java.nio.BufferUnderflowException;
import java.util.LinkedList;
import java.util.List;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * An abstract game board that supports most of the basic functionality that
 * are necessary for any concrete board games.
 * @author guangwei.zhu
 */
public abstract class GameBoard {

    /**
     * Constant prefix indicating an incoming new piece (used in networking)
     */
    public static final String MSG_PIECE = "PIECE ";
    // action actionLock
    protected final Object actionLock = new Object();   // Use this lock for board contents
    protected final Object playerLock = new Object();   // Use this lock for any player list op
    protected final int boardWidth, boardHeight;   // Board size
    protected final Object[][] blocks;           // Pile (fixed) blocks in the field
    protected LinkedList<Piece> nextPieces;     // Queue of next pieces
    protected Piece currentPiece;               // Current falling piece
    protected Point currentPosition;                // Position of current piece
    private Set<BoardListener> boardListeners = new HashSet<BoardListener>();
    private Set<Player> players = new HashSet<Player>();
    protected PieceProvider pieceProvider;
    protected boolean isStarted = false;
    protected PlayerListener playerActionHandler = new PlayerListener() {

        public void actionPerformed(PlayerActionEvent evt) {
            doAction(evt.action);
        }

        public void playerQuit(PlayerEvent evt) {
            unregisterPlayer(evt.getSource());
        }

        public void playerEnabled(PlayerEvent evt) {
        }

        public void playerDisabled(PlayerEvent evt) {
        }

        public void playerReady(PlayerEvent evt) {
        }
    };
    private boolean delayingRemoval;
    private Set<BoardListener> pendingRemovals = new HashSet<BoardListener>();

    private void delayRemove() {
        boardListeners.removeAll(pendingRemovals);
        delayingRemoval = false;
    }

    /**
     * Parse GameAction sent to this board
     * @param action the action that is sent to this board
     * @return true if the action is valid and performed; false otherwise
     */
    public abstract boolean doAction(GameAction action);

    /**
     * Eliminate parts of the blocks in the field if possible.
     * @return true if elimination is possible and executed; false otherwise
     */
    protected synchronized boolean eliminate() {
        return false;
    }

    /**
     * A routine that is run before the appearance of the next piece.
     * @return true if the game is to continue; false if the game is expected
     * to stop so that the next piece does not show up. (e.g. Game over)
     */
    protected synchronized boolean beforeNextPiece() {
        return true;
    }

    /**
     * A routine that is run right before fixing the current piece.
     * @return true if the game is to continue; false if the game is expected
     * to stop so that the piece is not fixed. (e.g. Game over)
     */
    protected synchronized boolean beforeFixingPiece() {
        return true;
    }

    /**
     * A routine that is run right after the appearance of the next piece.
     */
    protected synchronized void afterNextPiece() {
    }

    /**
     * Decide if the new player is allowed into this board.  By default,
     * a new main player will be rejected if there is already a main
     * player (not NPC) registered in the board.  For multi-player boards,
     * this function needs to be overridden.
     * @param newPlayer the player requesting to enter the board
     * @return true, by default, to allow player in; otherwise false
     */
    public synchronized boolean admitsNewPlayer(Player newPlayer) {
        if (!newPlayer.isMain()) {
            return true;    // Always allow NPC to join
            }
        for (Player registeredPlayer : players) {
            if (registeredPlayer.isMain()) {
                return false;   // Human player found
                }
        }
        return true;
    }

    /**
     * Implement this function to initialize the blocks variable.
     */
    protected abstract void initBlocks();

    /**
     * Create a generic game board with its width and height specified.
     */
    @SuppressWarnings({"unchecked"})
    public GameBoard(int width, int height) {
        nextPieces = new LinkedList<Piece>();
        blocks = new Object[height][width]; // unchecked conversion

        boardWidth = width;
        boardHeight = height;

        initBlocks();   // Initialize the elements in blocks
    }

    /**
     * Attach a player to this board so that the actions triggered by this player
     * will be listened and processed in this board.
     */
    public synchronized void registerPlayer(Player player) {
        synchronized (playerLock) {
            if (!admitsNewPlayer(player)) {
                throw new IllegalMonitorStateException("This player is not admissible.");
            }

            player.addPlayerListener(playerActionHandler);
            players.add(player);
            player.bindBoard(this);
        }
    }

    public synchronized void unregisterPlayer(Player player) {
        synchronized (playerLock) {
            player.removePlayerListener(playerActionHandler);
            players.remove(player);
            player.unbindBoard(this);
        }
    }

    public synchronized void setPieceProvider(PieceProvider provider) {
        this.pieceProvider = provider;
    }

    /**
     * Return if the game is still going on in this board.
     * @return true if the game is going on; false if the game is over or started.
     */
    public boolean isAlive() {
        return this.isStarted;
    }

    /**
     * Start the game by grabbing the first piece waiting in the list
     */
    public void start() {
        this.addNextPiece(pieceProvider.requestPiece());   // Ask for the first piece

        currentPiece = this.getNextPiece();
        currentPosition = new Point();

        isStarted = true;

        // Enable all registered players
        for (Player player : players) {
            player.setEnabled(true);
        }

        // notifyStarted();

        afterNextPiece();   // call the post-new-piece routine
    }

    /**
     * Declare gameover
     */
    public synchronized void gameover() {
        // Disable all players
        for (Player player : players) {
            player.setEnabled(false);
        }

        isStarted = false;

        notifyGameOver();

    }

    /**
     * Reset the Game Board, clear all related data
     */
    public synchronized void reset() {
        nextPieces.clear();
        initBlocks();

        isStarted = false;

        notifyReset();

    }

    /**
     * Handles the event when a player request to quit
     * @param player the quit player
     */
    protected void playerQuit(Player player) {
        try {       // Make silent if anything goes wrong
            this.unregisterPlayer(player);
            // this.gameover();
        } catch (Exception ex) {
        }
    }

    /**
     * Fix the current piece and set the new piece to be the next piece
     */
    @SuppressWarnings({"unchecked"})
    protected synchronized void fixPiece() {
        int i, j;
        int y, x;

        // Run before-fixing-piece routine
        if (!beforeFixingPiece()) {
            return;
        }

        if ((Integer) this.blocks[0][9] == 1) {
            System.out.println("HERE!");
        }

        // Fix the current piece
        for (i = 0; i < currentPiece.getHeight(); i++) {
            for (j = 0; j < currentPiece.getWidth(); j++) {
                if (currentPiece.getElement(j, i) != currentPiece.getEmptyElement()) {
                    x = j + currentPosition.x;
                    y = i + currentPosition.y;

                    // Check if piece can be fixed
                    if (x >= this.boardWidth || y >= this.boardHeight ||
                            this.blocks[y][x] != currentPiece.getEmptyElement()) {
                        throw new IllegalStateException();
                    }

                    this.blocks[y][x] = currentPiece.getElement(j, i);
                }
            }
        }

        // Run elimination routine
        this.eliminate();

        // Run before-new-piece routine
        if (!beforeNextPiece()) {
            return;
        }

        notifyPieceFixed(currentPiece, new Point(currentPosition));

        // Set the next piece to be the current piece;
        Piece nextPiece = this.getNextPiece();
        if (nextPiece == null) {
            throw new BufferUnderflowException();
        }

        currentPiece = nextPiece;

        // Run after-new-piece routine
        afterNextPiece();

    }

    /**
     * Add a BoardListener to the listener list
     * @param listener a BoardListener to be added
     */
    public synchronized void addBoardListener(BoardListener listener) {
        boardListeners.add(listener);
    }

    /**
     * Remove a BoardListener from the listener list
     * @param listener a BoardListener to be removed
     */
    public synchronized void removeBoardListener(BoardListener listener) {
        if (delayingRemoval) {
            pendingRemovals.add(listener);
        } else {
            boardListeners.remove(listener);
        }
    }

    /**
     * Notify all registered BoardListener about GameOver event
     */
    protected void notifyGameOver() {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardGameOver(new BoardEvent(this));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about Disposed event
     */
    protected void notifyDisposing() {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardDisposing(new BoardEvent(this));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about Changed event
     */
    protected void notifyStarted() {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardStarted(new BoardEvent(this));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about Changed event
     */
    protected void notifyReset() {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardReset(new BoardEvent(this));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about Changed event
     */
    protected void notifyChanged() {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardChanged(new BoardEvent(this));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about PieceFixed event
     */
    protected void notifyPieceFixed(Piece piece, Point position) {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardPieceFixed(new BoardPieceFixedEvent(this, piece, position));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about PieceFixed event
     */
    protected void notifyNextPieceChanged(Piece piece) {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardNextPieceChanged(new BoardPieceEvent(this, piece));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about Action event
     */
    protected void notifyActionTriggered(GameAction action) {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardActionTriggered(new BoardActionEvent(this, action));
        }
        delayRemove();
    }

    /**
     * Notify all registered BoardListener about elimination event
     */
    protected void notifyBlocksEliminated(Object eliminatedInfo) {
        delayingRemoval = true;
        for (BoardListener listener : boardListeners) {
            listener.boardBlocksEliminated(new BoardBlocksEliminatedEvent(this, eliminatedInfo));
        }
        delayRemove();
    }

    /**
     * Check if a piece overlaps am existing block in the board.
     * @param piece piece to be checked
     * @param position the top-left coordinate of the piece
     * @return true is the piece overlaps an existing block or goes out of
     * bound; false otherwise
     */
    @SuppressWarnings({"unchecked"})
    protected synchronized boolean isOverlapped(Piece piece, Point position) {
        int i, j;
        int x, y;

        for (i = 0; i < piece.getHeight(); i++) {
            for (j = 0; j < piece.getWidth(); j++) {
                if (piece.getElement(j, i) != piece.getEmptyElement()) {
                    x = j + position.x;
                    y = i + position.y;

                    // Check if piece exceeds the boundary
                    if (x < 0 || y < 0 || x >= this.boardWidth || y >= this.boardHeight) {
                        return true;
                    }

                    // Check if piece overlaps an existing block
                    if (this.blocks[y][x] != piece.getEmptyElement()) {
                        return true;
                    }

                }
            }
        }

        return false;

    }

    /**
     * Queue a piece which is to appear. The piece is placed at the end of
     * the queue.
     * @param piece piece that is to appear
     */
    public synchronized void addNextPiece(Piece piece) {
        if (piece == null) {
            return;
        }

        nextPieces.addLast(piece);
        if (nextPieces.size() == 1) {
            notifyNextPieceChanged(piece);
        }

    }

    /**
     * Obtain a readonly list of pieces that are to appear.
     * @return a readonly list of pieces waiting in the queue
     */
    public synchronized List queryNextPieces() {
        List<Piece> readonly_queue = Collections.unmodifiableList(nextPieces);

        return readonly_queue;
    }

    /**
     * Check the next piece without removing it from the queue.
     * @return the next piece which is about to appear
     */
    public synchronized Piece peekNextPiece() {
        if (nextPieces.isEmpty()) {
            return null;
        }

        return nextPieces.peekFirst();
    }

    /**
     * Retrieve the next piece and remove it from the queue.
     * @return the next piece which is about to appear
     */
    protected synchronized Piece getNextPiece() {
        Piece newPiece, oldPiece;

        if (nextPieces.isEmpty()) {
            return null;
        }

        oldPiece = nextPieces.pollFirst();
        newPiece = this.peekNextPiece();
        if (newPiece != null) {
            notifyNextPieceChanged(this.peekNextPiece());
        }

        return oldPiece;
    }

    /**
     * Retrieve the current piece in the GameBoard
     * @return the current piece
     */
    public synchronized Piece getCurrentPiece() {
        return this.currentPiece;
    }

    /**
     * Retrieve the current position of the falling piece
     * @return the current position
     */
    public synchronized Point getCurrentPosition() {
        return new Point(this.currentPosition);
    }

    /**
     * Create a cloned copy (deep copy) of the blocks array.  However, each
     * element is not field-wise cloned.
     * @return the cloned copy of blocks array
     */
    @SuppressWarnings({"unchecked"})
    public synchronized Object[][] createSnapshot() {
        Object[][] dataCopy = new Object[boardHeight][boardWidth];
        int i, j;

        for (i = 0; i < this.boardHeight; i++) {
            for (j = 0; j < this.boardWidth; j++) {
                dataCopy[i][j] = blocks[i][j];
            }

        }
        return dataCopy;
    }

    /**
     * Get the block at position (x,y)
     * @param x horizontal coordinate, from left to right
     * @param y vertical cooridnate, from top to bottom
     * @return the block at position (x,y)
     */
    @SuppressWarnings({"unchecked"})
    public synchronized Object getBlock(int x, int y) {
        return this.blocks[y][x];
    }

    public int getWidth() {
        return boardWidth;
    }

    public int getHeight() {
        return boardHeight;
    }

    /**
     * Terminate the functionality of this board and release any reference made
     * by this board (Players, Listeners...)  The board will no longer be usable
     * after calling this method.
     */
    public synchronized void dispose() {
        notifyDisposing();
        if (delayingRemoval) {
            pendingRemovals.addAll(boardListeners);
        } else {
            GameBoard.this.boardListeners.clear();
        }
        Player player;

        while (players.size() > 0) {
            player = players.iterator().next();
            try {
                player.setEnabled(false);
                this.unregisterPlayer(player);
            } catch (Exception ex) {
            }
        }
        players.clear();
    }
}
